package com.timk.goserver.server.services;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.hibernate.Query;
import org.hibernate.Session;

import com.timk.goserver.client.board.Board;
import com.timk.goserver.client.board.BoardController;
import com.timk.goserver.client.compatibility.Point;
import com.timk.goserver.client.model.ClientGameInfo;
import com.timk.goserver.client.model.ClientUserInfo;
import com.timk.goserver.client.services.GameService;
import com.timk.goserver.client.services.NoSessionException;
import com.timk.goserver.client.sgf.SGFUtils;
import com.timk.goserver.client.ui.screens.BoardPanel;
import com.timk.goserver.server.model.ServerGameInfo;
import com.timk.goserver.server.model.ServerUserInfo;
import com.timk.goserver.server.util.GSLogger;
import com.timk.goserver.server.util.GlobalProperties;
import com.timk.goserver.server.util.HibernateUtil;
import com.timk.goserver.server.util.Mailman;
import com.timk.goserver.server.util.RatingManager;

/**
 * GameService implementation
 * @author TKington
 *
 */
public class GameServiceImpl extends AbstractServiceServlet // NOPMD - Complexity
							implements GameService {
	private static final String MOVE_WAITING;
	
	static {
		MOVE_WAITING = "An igoserver move is waiting for you at "
			+ GlobalProperties.getProperty("base.url") + "Main.html";
	}
	
	public List getGames(String username, int filterType, int firstResult,
			int maxResults) throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		Query query = null;
		switch(filterType) {
		case GAME_FILTER_NEXT_TO_MOVE:
			query = hbSession.createQuery("from ServerGameInfo as g where g.nextToPlay = ?")
					.setString(0, username);
			break;
		case GAME_FILTER_RUNNING:
			query = hbSession.createQuery("select game from ServerGameInfo as game" +
					" inner join game.playerInfo as info" +
					" where game.state = " + GAME_STATE_RUNNING +
					" and info.name = ?" +
					" order by game.lastMoveTime desc")
					.setString(0, username);
			break;
		case GAME_FILTER_FINISHED:
			query = hbSession.createQuery("select game from ServerGameInfo as game" +
					" inner join game.playerInfo as info" +
					" where game.state = " + GAME_STATE_FINISHED +
					" and info.name = ?" +
					" order by game.lastMoveTime desc")
					.setString(0, username);
			break;
		default:
			GSLogger.logError("Unknown filter type: " + filterType);
			return null;
		}

		query.setFirstResult(firstResult);
		query.setMaxResults(maxResults);
		
		List result = query.list();
		ArrayList list = new ArrayList();
		for (Iterator iter = result.iterator(); iter.hasNext();) {
			ServerGameInfo sinfo = (ServerGameInfo) iter.next();
			list.add(sinfo.toClientGameInfo(hbSession, curUser, false));
		}

		hbSession.getTransaction().commit();
		return list;
	}

	public ClientGameInfo getGame(int gameId) throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo info = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameId));
			
			if(info == null) {
				return null;
			}
	
			return info.toClientGameInfo(hbSession, curUser, true);
		}
		finally {			
			hbSession.getTransaction().commit();
		}
	}

	public int setNotes(int gameId, String notes) throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo info = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameId));
			
			if(info == null) {
				return STATUS_NO_SUCH_GAME;
			}
	
			info.setNotes(curUser, notes);
			
			return STATUS_SUCCESS;
		}
		finally {			
			hbSession.getTransaction().commit();
		}
	}

	public int playMoves(int gameid, int firstMoveNum, List moves, String comment)
			throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo gameInfo = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameid));
			
			if(gameInfo == null) {
				return STATUS_NO_SUCH_GAME;
			}
			
			if(!curUser.equals(gameInfo.getNextToPlay())) {
				return STATUS_WRONG_USER;
			}
			
			return playMoves(hbSession, curUser, gameInfo, moves, firstMoveNum, comment);
		}
		finally {
			hbSession.getTransaction().commit();
		}
	}
	
	public int submitDeadStones(int gameid, Set deadStones, String comment)
			throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo gameInfo = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameid));
			
			if(gameInfo == null) {
				return STATUS_NO_SUCH_GAME;
			}
			
			if(!curUser.equals(gameInfo.getNextToPlay())) {
				return STATUS_WRONG_USER;
			}
			
			return submitDeadStones(gameInfo, deadStones, comment);
		}
		finally {
			hbSession.getTransaction().commit();
		}
	}
	
	private int submitDeadStones(ServerGameInfo game, Set deadStones, String comment)
			throws NoSessionException {
		if(game.getDeadStones() == null) {
			BoardController controller = new BoardController();
			controller.loadGame(game.getSize(), game.getSgf(), game.getDeadStones());
			
			controller.pass();
			controller.setComment(getCurrentUsername(), comment);
			game.setSgf(controller.getSgf());
		} else {
			boolean match = compareDeadStones(
					SGFUtils.stringToPoints(game.getDeadStones()), deadStones);
			
			if(match) {
				setGameResult(game, comment);
				return STATUS_SUCCESS;
			}
		}
		
		game.setDeadStones(SGFUtils.pointsToString(deadStones));
		
		List playerNames = game.getPlayerNames();
		int curPlayerIndex = game.getNextPlayerIndex();
		curPlayerIndex = (curPlayerIndex + 1) % playerNames.size();
		game.setNextToPlay(curPlayerIndex);
		game.setLastMoveTime(new Date());
		
		return STATUS_SUCCESS;
	}
	
	public int resumeGame(int gameid) throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo gameInfo = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameid));
			
			if(gameInfo == null) {
				return STATUS_NO_SUCH_GAME;
			}
			
			if(!curUser.equals(gameInfo.getNextToPlay())) {
				return STATUS_WRONG_USER;
			}
			
			return resumeGame(gameInfo);
		}
		finally {
			hbSession.getTransaction().commit();
		}
	}
	
	private int resumeGame(ServerGameInfo game) {
		BoardController controller = new BoardController();
		controller.loadGame(game.getSize(), game.getSgf(),
				game.getDeadStones());
		
		int curPlayerIndex = game.getNextPlayerIndex();
		int curPlayerColor = (curPlayerIndex % 2 == 0) ? 1 : -1;
		controller.removeLastMove();
		if(controller.getCurPlayer() != curPlayerColor) {
			controller.removeLastMove();
		}
		
		game.setSgf(controller.getSgf());
		game.setDeadStones(null);
		game.setLastMoveTime(new Date());
		
		return STATUS_SUCCESS;
	}

	private void setGameResult(ServerGameInfo game, String comment)
			throws NoSessionException {
		BoardController controller = new BoardController();
		controller.loadGame(game.getSize(), game.getSgf(), game.getDeadStones());
		
		int blackScore = controller.getBlackScore();
		double whiteScore = controller.getWhiteScore() + game.getKomi();
		
		String result;
		if(blackScore > whiteScore) {
			result = "B+" + (blackScore - whiteScore);
		} else {
			result = "W+" + (whiteScore - blackScore);
		}
		
		controller.setResult(result, game.getKomi());
		game.setSgf(controller.getSgf());
		
		setGameResult(game, comment, result);
	}
	
	private void setGameResult(ServerGameInfo game, String comment, String result)
			throws NoSessionException {
		game.setResult(result);
		game.clearNextToPlay();
		game.setState(GAME_STATE_FINISHED);
		game.exposeHiddenComments();
		game.setLastMoveTime(new Date());
		
		StringBuffer text = new StringBuffer(50);
		
		if(comment != null && comment.length() > 0) {
			text.append(getCurrentUsername()).append(": ").append(comment).append("\n\n");
		}
		
		text.append("Black: ");
		game.getBlackNames(text);
		text.append("\nWhite: ");
		game.getWhiteNames(text);
		text.append("\nResult: ");
		text.append(game.getResult());
		
		Session hbSession = HibernateUtil.getSessionFactory().getCurrentSession();
		List allPlayers = MessageServiceImpl.getOtherPlayers(hbSession,
				"", game.getPlayerNames());
		MessageServiceImpl.sendMessage(hbSession, "System", allPlayers,
				"Game Result", text.toString());
		
		if(game.isRated()) {
			updatePlayerRatings(game);
		}
	}
	
	private void updatePlayerRatings(ServerGameInfo game) {
		List playerNames = game.getPlayerNames();
		
		Session hbSession = HibernateUtil.getSessionFactory().getCurrentSession();
		
		String blackName = (String)playerNames.get(0);
		ServerUserInfo black = ServerUserInfo.findUser(blackName, hbSession);
		
		String whiteName = (String)playerNames.get(1);
		ServerUserInfo white = ServerUserInfo.findUser(whiteName, hbSession);

		boolean blackWon = game.getResult().charAt(0) == 'B';
		
		RatingManager.updateRatings(black, white, game.getHandicap(),
				game.getKomi(), blackWon);
	}
	
	private boolean compareDeadStones(List oldDead, Set newDead) {
		if(oldDead.size() != newDead.size()) {
			return false;
		}
		
		for (Iterator iter = oldDead.iterator(); iter.hasNext();) {
			Point point = (Point) iter.next();
			if(!newDead.contains(point)) {
				return false;
			}
		}
		
		return true;
	}

	public int deleteGame(int gameid) throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo info = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameid));
			
			if(info == null) {
				return STATUS_NO_SUCH_GAME;
			}
			
			BoardController controller = new BoardController();
			controller.loadGame(info.getSize(), info.getSgf(), info.getDeadStones());
			
			if(controller.getNumFinalMoves() >= 20) {
				return STATUS_CANT_DELETE;
			}
			
			hbSession.delete(info);
			
			List otherPlayers = MessageServiceImpl.getOtherPlayers(hbSession,
					curUser, info.getPlayerNames());
			MessageServiceImpl.sendMessage(hbSession, curUser, otherPlayers,
					"Game Deleted", curUser + " has deleted your game.");
			
			return STATUS_SUCCESS;
		}
		finally {			
			hbSession.getTransaction().commit();
		}
	}

	public int resignGame(int gameid, String comment) throws NoSessionException {
		String curUser = getCurrentUsername();
		
		Session hbSession = HibernateUtil.startTrans();
		
		try {
			ServerGameInfo game = (ServerGameInfo)hbSession
					.get(ServerGameInfo.class, new Integer(gameid));
			
			if(game == null) {
				return STATUS_NO_SUCH_GAME;
			}
			
			if(game.getState() != GAME_STATE_RUNNING) {
				return STATUS_ILLEGAL_STATE_TRANSITION;
			}

			if(!curUser.equals(game.getNextToPlay())) {
				return STATUS_WRONG_USER;
			}
			
			int index = game.getNextPlayerIndex();
			
			String result;
			if(index % 2 == 0) {
				result = "W+R";
			} else {
				result = "B+R";
			}
			
			setGameResult(game, comment, result);
			
			return STATUS_SUCCESS;
		}
		finally {			
			hbSession.getTransaction().commit();
		}
	}
	
	/**
	 * Sends an email notification to the player whose turn it is
	 * @param hbSession the active session
	 * @param game the game
	 * @param board the Board
	 */
	public static void sendMoveNotification(Session hbSession,
			ServerGameInfo game, Board board) {
		ArrayList usersToNotify = new ArrayList();
		ServerUserInfo user = ServerUserInfo.findUser(game.getNextToPlay(), hbSession);

		StringBuffer text;
		boolean isHTML = false;
		
		switch(user.getNotification()) {
		case ClientUserInfo.NOTIFICATION_NONE:
			return;
		case ClientUserInfo.NOTIFICATION_NOTIFY:
			text = new StringBuffer(100);
			text.append(MOVE_WAITING);
			break;
		case ClientUserInfo.NOTIFICATION_FULL_BOARD:
			isHTML = true;
			
			text = new StringBuffer(20000);
			text.append(MOVE_WAITING);
			appendBoardHTML(text, board);
			break;
		default:
			GSLogger.logError("Unknown notification type. user="
					+ game.getNextPlayerIndex() + " type=" + user.getNotification());
			return;
		}
		
		
		usersToNotify.add(user.getEmail());
		Mailman.sendMessage(usersToNotify, "Igoserver Notification",
				text.toString(), isHTML);
	}
	
	private static void appendBoardHTML(StringBuffer buf, Board board) {
		String baseURL = GlobalProperties.getProperty("base.url");
		buf.append("<p/><table border=0 background=\"");
		buf.append(baseURL);
		buf.append("images/wood1.gif\">\n<tr><td>\n<table border=0 cellpadding=0 cellspacing=0 background=\" \">\n");
		
		int size = board.getSize();
		for(int y = 0; y < size; y++) {
			buf.append("  <tr>\n");
			
			for(int x = 0; x < size; x++) {
				String url = BoardPanel.getImageURL(board, x, y);
				char altChar = BoardPanel.getAltChar(board, x, y);
				buf.append("    <td><img src=\"");
				buf.append(baseURL);
				buf.append(url);
				buf.append("\" alt=\"").append(altChar).append("\"/></td>\n");
			}
			
			buf.append("  </tr>\n");
		}
		buf.append("</table>\n</td></tr></table>");
	}

	private int playMoves(Session hbSession, String curUser,	// NOPMD - Complexity 
			ServerGameInfo game, List moves, int firstMoveNum,
			String comment) {
		BoardController controller = new BoardController();
		controller.loadGame(game.getSize(), game.getSgf(), game.getDeadStones());
		
		if(controller.getNumFinalMoves() + 1 != firstMoveNum) {
			return STATUS_WRONG_MOVE_NUMBER;
		}
		
		int curPlayerIndex = game.getNextPlayerIndex();
		int thisPlayerIndex = curPlayerIndex;
		
		List [] lists = futureMovesToPointLists(game.getFutureMoves());
		lists[curPlayerIndex] = new ArrayList(moves);
		boolean interactiveMove = true;
		
		while(true) {
			if(lists[curPlayerIndex].size() == 0) {
				break;
			}
			
			Point move = (Point)lists[curPlayerIndex].get(0);
			
			if(move.x == BoardController.PASS_COORD) {
				controller.pass();
			} else {
				if(!controller.playStone(move.x, move.y)) {
					return STATUS_ILLEGAL_MOVE;
				}
			}
			
			int curMove = game.getMoveNumber() + 1;
			game.setMoveNumber(curMove);

			//	Only the first move is interactive, the rest are
			//	auto-played.
			if(interactiveMove) {
				controller.setComment(curUser, comment);
				game.setLastInteractiveMove(curPlayerIndex, curMove);
				interactiveMove = false;
			}
			
			for(int i = 0; i < lists.length; i++) {
				if(lists[i].isEmpty()) {
					continue;
				}
				
				Point predictedMove = (Point)lists[i].get(0);
				if(predictedMove.equals(move)) {
					lists[i].remove(0);
				} else {
					lists[i].clear();
				}
			}
			
			curPlayerIndex = (curPlayerIndex + 1) % game.getNumPlayers();
		}
		
		game.setFutureMoves(futureMovesToStringList(lists));
		game.setNextToPlay(curPlayerIndex);
		game.setSgf(controller.getSgf());
		game.setLastMoveTime(new Date());
		
		if(curPlayerIndex == thisPlayerIndex) {
			return STATUS_AUTO_REPLY;
		}
		
		sendMoveNotification(hbSession, game, controller.getBoard());
		return STATUS_SUCCESS;
	}
	
	private List [] futureMovesToPointLists(List futureMoves) {
		List [] lists = new ArrayList[futureMoves.size()];
		
		for(int i = 0; i < lists.length; i++) {
			lists[i] = SGFUtils.stringToPoints((String)futureMoves.get(i));
		}
		
		return lists;
	}
	
	private List futureMovesToStringList(List [] lists) {
		List stringList = new ArrayList();
		
		for(int i = 0; i < lists.length; i++) {
			stringList.add(SGFUtils.pointsToString(lists[i]));
		}
		
		return stringList;
	}
}
